#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<windows.h>
//第22讲：⽂件操作+第23讲：编译和链接
//第三组:
//fprintf:格式化输出函数
//int fprintf(FILE* stream, const char* format, ...);
//struct Stu
//{
//	char name[20];
//	int age;
//	float score;
//};
//int main()
//{
//	struct Stu s1 = { "zhangsan",20,87.5f };
//	//打开文件:
//	FILE*pf=fopen("test.txt", "w");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	//写文件--是以文本的形式写进去的
//	fprintf(pf, "%s %d %f", s1.name, s1.age, s1.score);
//	//关闭文件:
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}

//fscanf:格式化输⼊函数
//int fscanf ( FILE * stream, const char * format, ... );
//struct Stu
//{
//	char name[20];
//	int age;
//	float score;
//};
//int main()
//{
//	struct Stu s1 = { 0 };
//	//打开文件:
//	FILE*pf=fopen("test.txt", "r");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	//读文件
//	fscanf(pf, "%s %d %f", s1.name, &(s1.age), &(s1.score));
//	//打印在屏幕上面看看读取成功了没有
//	/*printf("%s %d %f", s1.name, s1.age, s1.score);*/
//	//也可以用fprintf来打印在屏幕上，用的是标准输出流:
//	fprintf(stdout, "%s %d %f", s1.name, s1.age, s1.score);
//
//	//关闭文件:
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}

//前三组是输出的函数就适用于所有输出流，包括文件流，标准输出流-stdout
//前三组是输入的函数就适用于所有输入流，包括文件流，标准输入流-stdin
//所以这些函数不仅可以在文件里输入输出，还可以在键盘上输入，在屏幕上输出
//例如:
//int main()
//{
//	fputc('c',stdout);		//在屏幕上输出
//	return 0;
//}
//int main()
//{
//	char ret=fgetc(stdin);	//在屏幕上输入
//	printf("%c", ret);
//	return 0;
//}

//scanf-从标准输入流上读取格式化的数据
//fscanf-从指定的输入流上读取格式化的数据(可以是所有的输入流,包括文件流和标准输入流)

//printf-把数据以格式化的形式打印在标准输出流上
//fprintf-把数据以格式化的形式打印在指定的输出流上（可以是所有输出流）

//第四组:
//sprintf和sscanf:
//struct Stu
//{
//	char name[20];
//	int age;
//	float score;
//};
//int main()
//{
//	char arr[200] = { 0 };
//	struct Stu s1 = { "zhangsan",20,65.5f };
//	//sprintf:把格式化的数据转化成字符串
//	//int sprintf(char* str, const char* format, ...);
//	sprintf(arr, "%s %d %f", s1.name, s1.age, s1.score);//格式化数据转化为字符串
//	printf("%s\n", arr);
//	//sscanf:在字符串中读取格式化的数据
//	//int sscanf ( const char * s, const char * format, ...)
//	struct Stu s2 = { 0 };
//	sscanf(arr, "%s %d %f", s2.name, &(s2.age), &(s2.score));//字符串转化为格式化的数据
//	printf("%s %d %f\n", s2.name, s2.age, s2.score);
//	return 0;
//}

//第五组:
//fwrite ⼆进制输出 适用于:⽂件输出流
//size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
//返回值是成功写入元素的个数
//把ptr指向的内容写入stream中
//size是每个元素的大小
//count是你要写入元素的个数
//int main()
//{
//	int arr[5] = { 1,2,3,4,5 };
//	//打开文件:
//	FILE* pf = fopen("test.txt", "wb");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	//写文件(以二进制形式写入):
//	fwrite(arr,sizeof(int),sizeof(arr)/sizeof(arr[0]),pf);
//	//关闭文件:
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}

//fread ⼆进制输⼊ ⽂件输⼊流
//size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
//返回值是成功读入的元素个数
//把stream的数据放入ptr指向的空间里(和刚刚的是逆过程)
//int main()
//{
//	int arr[5] = { 1,2,3,4,5 };
//	int arr1[5] = { 0 };
//	//打开文件:
//	FILE* pf = fopen("test.txt", "rb");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	fwrite(arr, sizeof(int), sizeof(arr) / sizeof(arr[0]), pf);
//	////读文件(以二进制形式读取):
//	//fread(arr1, sizeof(int), sizeof(arr) / sizeof(arr[0]), pf);
//	//for (int i = 0; i < 5; i++)
//	//	printf("%d ", arr1[i]);
//	
//	//当然也可以用这种方式边读边打印:
//	int i = 0;
//	while (fread(arr1 + i, sizeof(int), 1, pf))
//	{
//		printf("%d ", arr1[i]);
//		i++;
//	}
//	//关闭文件:
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}



//6. ⽂件的随机读写
//6.1 fseek:根据⽂件指针的位置和偏移量来定位⽂件指针（⽂件内容的光标)
//int fseek ( FILE * stream, long int offset, int origin );
//stream:你要读取的文件
//offset:偏移量
//origin:光标起始位置
//光标的起始位置有三种选择:
//SEEK_SET	Beginning of file
//SEEK_CUR	Current position of the file pointer
//SEEK_END	End of file*
//int main()
//{
//	//先假设stream里是abcdefg
//	FILE*pf=fopen("test.txt", "r");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	//读取数据:
//	int ch=fgetc(pf);
//	printf("%c\n", ch);//a
//	//移动光标:
//	fseek(pf, 4, SEEK_CUR);
//	//再读取一次
//	ch=fgetc(pf);
//	printf("%c\n", ch);//f
//	//再移动:
//	fseek(pf, -2, SEEK_END);
//	ch = fgetc(pf);
//	printf("%c\n", ch);//f
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}


//6.2 ftell
//long int ftell ( FILE * stream );
//返回⽂件指针相对于起始位置的偏移量
//int main()
//{
//	FILE* pf = fopen("test.txt", "r");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	fseek(pf, 0, SEEK_END);		
//	printf("%d", ftell(pf));//这样相当于能求出文件长度
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}
//


//6.3 rewind
//让⽂件指针的位置回到⽂件的起始
//void rewind(FILE* stream);
//int main()
//{
//	FILE* pf = fopen("test.txt", "r");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	char ch = fgetc(pf);//读完后光标移到下一个位置
//	printf("%c", ch);//a
//	rewind(pf);//光标回到起始位置
//	ch = fgetc(pf);
//	printf("%c", ch);//a
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}



//7. ⽂件读取结束的判定
//7.1 被错误使⽤的 feof
//牢记：在⽂件读取过程中，不能⽤feof函数的返回值直接来判断⽂件的是否结束。
//feof 的作⽤是：当⽂件读取结束的时候，判断是读取结束的原因是否是：遇到⽂件尾结束。
//假设文件现在读取结束了
//但是是什么原因读取结束的呢？
//1.有可能遇到文件末尾    --  feof   用来判断已经读取完的情况下是不是遇到了文件末尾而结束
//2.读取的时候发生了错误  --  ferror 用来判断已经读取完的情况下是不是遇到了某种错误而结束
//打开一个流的时候，这个流上有两个标记值：
//1.是否遇到文件末尾
//2.是否发生错误

//⽂本⽂件读取是否结束，判断返回值是否为 EOF （ fgetc ），或者 NULL （ fgets ）
//例如：
//• fgetc 判断是否为 EOF .
//• fgets 判断返回值是否为 NULL .
//再复习一下:
//如果fgetc读取正常，就返回读取到字符的ASCII码值
//如果读取过程中遇到文件末尾或者发生错误，就返回EOF
//如果fgets读取正常，就返回存储读取到字符串的字符数组的地址
//如果读取的过程中遇到文件末尾或者发生错误，返回NULL
//int main()
//{
//	FILE* pf = fopen("test.txt", "r");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	//读取文件:
//	char ch = 0;
//	while ((ch = fgetc(pf)) != EOF)
//	{
//		printf("%c\n", ch);
//	}
//	//读取结束来判断一下是遇到错误结束还是正常结束:
//	if (feof(pf))
//	{
//		printf("正常结束");
//	}
//	else if (ferror(pf))
//	{
//		perror("fgetc");
//	}
// 	fclose(pf);
//pf = NULL;
//	return 0;
//}

//如果发生错误
//例子：以读的方式来写:
//int main()
//{
//	FILE* pf = fopen("test.txt", "r");
//	if (pf == NULL)
//	{
//		perror("fopen");
//		return 1;
//	}
//	//写文件:
//	char ch = 0;
//	for (ch = 'a'; ch <= 'z'; ch++)
//	{
//		fputc(ch, pf);
//	}
//	//读取结束来判断一下是遇到错误结束还是正常结束:
//	if (feof(pf))
//	{
//		printf("正常结束");
//	}
//	else if (ferror(pf))
//	{
//		perror("fgetc");
//		//显示:fgetc: Bad file descriptor 就是错误的文件描述，把以“读”的方式打开却进行写
//	}
//	fclose(pf);
//	pf = NULL;
//	return 0;
//}


//拷贝内容: test1.txt ---> test2.txt
//int main()
//{
//	FILE* pfin = fopen("test1.txt", "r");
//	if (pfin == NULL)
//	{
//		perror("fopen:test1.txt");
//		return 1;
//	}
//	FILE* pfout= fopen("test2.txt", "w");
//	if (pfout == NULL)
//	{
//		fclose(pfin);
//		perror("fopen:test2.txt");
//		return 1;
//	}
//	int ch = 0;
//	while ((ch = fgetc(pfin) != EOF))
//	{
//		fputc(ch, pfout);
//	}
//	fclose(pfin);
//	pfin = NULL;
//	fclose(pfout);
//	pfout = NULL;
//	return 0;
//}//??????????????????????????????????????????????????????为什莫成功补了，何意味？

//⼆进制⽂件的读取结束判断，判断返回值是否⼩于实际要读的个数。
//例如：
//• fread判断返回值是否⼩于实际要读的个数。



//8. ⽂件缓冲区
//ANSIC 标准采⽤“缓冲⽂件系统” 处理的数据⽂件的，所谓缓冲⽂件系统是指系统⾃动地在内存中为
//程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓
//冲区，装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据，则从磁盘⽂件中读取数据输
//⼊到内存缓冲区（充满缓冲区），然后再从缓冲区逐个地将数据送到程序数据区（程序变量等）。缓
//冲区的⼤⼩根据C编译系统决定的
//VS2022 WIN11环境测试
//我们可以观察到现象:
//int main()
//{
//	FILE* pf = fopen("test.txt", "w");
//	fputs("abcdef", pf);//先将代码放在输出缓冲区
//	printf("睡眠10秒-已经写数据了，打开test.txt⽂件，发现⽂件没有内容\n");
//	Sleep(10000);
//	printf("刷新缓冲区\n");
//	fflush(pf);//刷新缓冲区时，才将输出缓冲区的数据写到⽂件（磁盘）
//	//注：fflush 在⾼版本的VS上不能使⽤了
//	printf("再睡眠10秒-此时，再次打开test.txt⽂件，⽂件有内容了\n");
//	Sleep(10000);
//	fclose(pf);
//	//注：fclose在关闭⽂件的时候，也会刷新缓冲区
//	pf = NULL;
//	return 0;
//}
//这⾥可以得出⼀个结论：
//因为有缓冲区的存在，C语⾔在操作⽂件的时候，需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。



//第23讲：编译和链接
//test.c - 源文件
//test.h - 头文件
//代码都是文本信息，电脑不能直接识别c语言代码，计算机能够执行的是二进制指令
//所以C语言代码要转换成二进制指令，这一过程是由编译器做到的
//1. 翻译环境和运⾏环境
//在 ANSI C 的任何⼀种实现中，存在两个不同的环境。
//第1种是翻译环境，在这个环境中源代码被转换为可执⾏的机器指令（⼆进制指令）。
//第2种是运行环境（通常指我们的操作系统），它⽤于实际执⾏代码。

//2. 翻译环境
//那翻译环境是怎么将源代码转换为可执⾏的机器指令的呢？这
//其实翻译环境是由编译和链接两个⼤的过程组成的，⽽编译⼜可以分解成：预处理（有些书也叫预编译）、编译、汇编三个过程。
//⼀个C语⾔的项⽬中可能有多个.c ⽂件⼀起构建，那多个.c ⽂件如何⽣成可执⾏程序呢？
//• 多个.c⽂件单独经过编译器，编译处理⽣成对应的⽬标⽂件。
//• 注：在Windows环境下的⽬标⽂件的后缀是.obj ，Linux环境下⽬标⽂件的后缀是.o
//• 多个⽬标⽂件和链接库⼀起经过链接器处理⽣成最终的可执⾏程序。
//• 链接库是指运⾏时库(它是⽀持程序运⾏的基本函数集合)或者第三⽅库。


//2.1 预处理（预编译）
//在预处理阶段，源⽂件和头⽂件会被处理成为.i 为后缀的⽂件。
//预处理阶段主要处理那些源⽂件中#开始的预编译指令。⽐如：#include, #define，处理的规则如下：
//• 将所有的 #define 删除，并展开所有的宏定义。
//• 处理所有的条件编译指令，如： #if、#ifdef、#elif、#else、#endif 。
//• 处理#include 预编译指令，将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进
//⾏的，也就是说被包含的头⽂件也可能包含其他⽂件。
//• 删除所有的注释
//• 添加⾏号和⽂件名标识，⽅便后续编译器⽣成调试信息等。
//• 或保留所有的#pragma的编译器指令，编译器后续会使⽤。
//经过预处理后的.i ⽂件中不再包含宏定义，因为宏已经被展开。并且包含的头⽂件都被插⼊到.i
//⽂件中。所以当我们⽆法知道宏定义或者头⽂件是否包含正确的时候，可以查看预处理后的.i ⽂件来确认。


//2.2 编译
//编译过程就是将预处理后的⽂件进⾏⼀系列的：词法分析、语法分析、语义分析及优化，⽣成相应的汇编代码⽂件。
//核心功能就是把C代码转化为汇编代码,生成test.s文件
//2.2.1 词法分析
//将源代码程序被输⼊扫描器，扫描器的任务就是简单的进⾏词法分析，把代码中的字符分割成⼀系列
//的记号（关键字、标识符、字⾯量、特殊字符等）
//2.2.2 语法分析
//接下来语法分析器，将对扫描产⽣的记号进⾏语法分析，从⽽产⽣语法树。这些语法树是以表达式为
//节点的树。
//2.2.3 语义分析
//由语义分析器来完成语义分析，即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分
//析。静态语义分析通常包括声明和类型的匹配，类型的转换等。这个阶段会报告错误的语法信息。

//2.3 汇编
//汇编器是将汇编代码转转变成机器可执⾏的指令，每⼀个汇编语句⼏乎都对应⼀条机器指令。就是根
//据汇编指令和机器指令的对照表⼀⼀的进⾏翻译，也不做指令优化。
//生成test.o目标文件，把汇编代码转换成了机器指令

//2.4 链接
//链接是⼀个复杂的过程，链接的时候需要把⼀堆⽂件链接在⼀起才⽣成可执⾏程序。
//链接过程主要包括：地址和空间分配，符号决议和重定位等这些步骤。
//链接解决的是⼀个项⽬中多⽂件、多模块之间互相调⽤的问题。
//⽐如：
//在⼀个C的项⽬中有2个.c⽂件（ test.c 和 add.c ）



//3. 运⾏环境
//1. 程序必须载⼊内存中。在有操作系统的环境中：⼀般这个由操作系统完成。在独⽴的环境中，程序
//的载⼊必须由⼿⼯安排，也可能是通过可执⾏代码置⼊只读内存来完成。
//2. 程序的执⾏便开始。接着便调⽤main函数。
//3. 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈（stack），存储函数的局部变量和返回
//地址。程序同时也可以使⽤静态（static）内存，存储于静态内存中的变量在程序的整个执⾏过程
//⼀直保留他们的值。
//4. 终⽌程序。正常终⽌main函数；也有可能是意外终⽌。